// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Provides a visitor class that finds and documents class and interface
 * methods.
 */

var Token = com.google.javascript.rhino.Token,
    NodeUtil = com.google.javascript.jscomp.NodeUtil,
    logger = require('ringo/logging').getLogger('MethodV');



/**
 * A node visitor that finds and documents class and interface methods.
 *
 * @constructor
 * @implements {Documenter.Visitor}
 */
var MethodV = exports.MethodV = function() {
  /**
   * The next visitor in the chain.
   *
   * @type {Documenter.Visitor}
   * @private
   */
  this.nextVisitor_ = null;
};


/**
 * @param {Documenter.Visitor} visitor The next visitor in the chain.
 */
MethodV.prototype.setNext = function(visitor) {
  this.nextVisitor_ = visitor;
};


/**
 * Determine if a node represents an assignment to a prototype property and
 * is a function.  If the right-hand value is not a function, this will
 * also check whether it is goog.nullFunction or goog.abstractMethod and will
 * document the assignment as a method if so.
 *
 * @param {Node} node The node to visit.
 * @parma {DocInfo} docInfo The page's documentation info object.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 */
MethodV.prototype.visit = function(node, docInfo, jsdoc) {
  var names;
  if (jsdoc && (names = this.isMethodAssignment(node, docInfo, jsdoc))) {
    var classDesc = docInfo.getClassDescriptor(names.className);
    
    if (classDesc !== null) {
      var methods = classDesc.methods;

      if (! methods) {
        methods = classDesc.methods = {};
      }

      var methodDesc = methods[names.methodName] = {};
      methodDesc.desc = jsdoc.getBlockDescription();
      methodDesc.isOverride = jsdoc.isOverride();
      
      if (jsdoc.isDeprecated()) {
        methodDesc.deprecationReason = jsdoc.getDeprecationReason() || '';
      }

      methodDesc.visibility = docInfo.visibility(jsdoc.getVisibility());

      if (MethodV.isAbstractMethod(node)) {
        methodDesc.isAbstractMethod = true;
      }

      var paramCount = jsdoc.getParameterCount();
      if (paramCount > 0) {
        var params = jsdoc.getParameterNames().iterator(),
            paramDescs = methodDesc.params = [];
  
        while (params.hasNext()) {
          var param = params.next();
          paramDescs.push({
            name: param,
            type: docInfo.resolveType(jsdoc.getParameterType(param)),
            desc: jsdoc.getDescriptionForParameter(param)
          });
        }
      }
  
      if (jsdoc.hasReturnType()) {
        methodDesc.returns = {
          type: docInfo.resolveType(jsdoc.getReturnType()),
          desc: jsdoc.getReturnDescription()
        }
      }
    } else { // classDesc === null
      logger.warn('Skipping method declaration on undeclared class "' +
                  names.className + ' at ' + NodeUtil.getSourceFile(node));
    }
  }

  if (this.nextVisitor_) {
    this.nextVisitor_.visit(node, docInfo, jsdoc);
  }
};


/**
 * Determine if a node is an assignment of a function to a prototype property.
 * Will also return true if the RHS is a qualified name AND the JSDoc includes
 * function-esque annotations (param, return).
 *
 * @param {Node} node The node to inspect.
 * @param {DocInfo} docInfo The DocInfo object for the file being parsed.
 * @param {JSDocInfo} jsdoc The jsdoc for the given node or null if no
 *    doc comments were attached.
 *
 * @return {(null|{className: string, methodName: string})} A record object
 *    containing the class and method name, or null if the node is not a method
 *    assignment.
 */
MethodV.prototype.isMethodAssignment = function(node, docInfo, jsdoc) {
  // Normal class methods defined on the prototype.
  var parent = node.getParent();
  if (node.getType() === Token.ASSIGN) {
    var left = node.getFirstChild(),
        lhsName = left.getQualifiedName() || '',
        right = left.getNext();

    if (lhsName.indexOf('prototype') === -1) {
      return null;
    }

    var names = lhsName.split('.prototype.');

    var rhsType = right.getType();
    if (rhsType !== Token.FUNCTION) {
      // If we're not assigning a function, see if the right-hand side is
      // one of the commonly assigned function references.

      var rhsName = right.getQualifiedName() || '';

      if (validMethodFunctions.indexOf(rhsName) === -1 &&
          rhsName.indexOf('goog.functions') === -1 &&
          ((! jsdoc) || ! MethodV.hasFunctionAnnotations(jsdoc))) {
            // RHS object is not one of the common function references
            // and the assignment has no function-relevant JSDoc tags.

            return null;
      }
    }

    // At this point, we know we're assigning either a function expression,
    // one of the commonly assigned function references, or something with
    // function annotations.
    return {
      className: names[0],
      methodName: names[1]
    };
  }

  // Some interface or abstract/super-class methods are not declared via
  // assigment, but only have a top-level qualified name.  For example:
  //
  // /closure-library/closure/goog/disposable/idisposable.js
  // goog.disposable.IDisposable.prototype.isDisposed;
  //
  // /closure-library/closure/goog/editor/plugin.js
  // goog.editor.Plugin.prototype.getTrogClassId;
  //
  // In these situations, we need to determine whether this is a method or
  // a property.
  else if (node.getType() === Token.GETPROP &&
           node.getParent().getType() === Token.EXPR_RESULT) {
    var name = node.getQualifiedName();

    if (name.indexOf('.prototype.') > -1) {
      var names = name.split('.prototype.');

      if (docInfo.isDeclaredInterface(names[0]) ||
          docInfo.isDeclaredClass(names[0])) {

        var isProperty = false,
            isMethod = false;

        if (jsdoc) {
          isProperty = jsdoc.hasType();
          isMethod = MethodV.hasFunctionAnnotations(jsdoc);
        }

        if (isProperty && (! isMethod)) {
          return null;
        }
        else if (isMethod && !isProperty) {
          return {
            className: names[0],
            methodName: names[1]
          };
        }
        else if (isProperty && isMethod) {
          if (this instanceof MethodV) {
            logger.warn(name + ' has both method and property annotations.  ' +
                      'Assuming it is a method.');
          }

          return {
            className: names[0],
            methodName: names[1]
          };
        }
        else if (!isProperty && !isMethod) {
          if (this instanceof MethodV) {
            logger.warn(name + ' could be a property or method.  ' +
                        'Assuming it is a method.');
          }

          return {
            className: names[0],
            methodName: names[1]
          };
        }
      }
    }
  }

  return null;
};


/**
 * Static function to determine whether an assignment is an assignment of
 * "goog.abstractMethod" to a prototype property.
 *
 * @param {Node} node A Node of type Token.ASSIGN.
 * @return {boolean} Whether or not this is an abstract method declaration.
 */
MethodV.isAbstractMethod = function(node) {
  if (node.getType() !== Token.ASSIGN) {
    return false;
  }

  var assignee = node.getChildAtIndex(1);
  
  if (assignee.getType() === Token.GETPROP &&
      assignee.getQualifiedName() === 'goog.abstractMethod') {
    return true;
  }
  else {
    return false;
  }
};


/**
 * Whether or not a given JSDocInfo object contains function-related
 * annotations.
 *
 * @param {JSDocInfo} jsdoc The JSDocInfo to look at.
 * @return {boolean} Whether the given JSDocInfo contains function-related
 *     annotations.
 */
MethodV.hasFunctionAnnotations = function(jsdoc) {
  var noType = (! jsdoc.hasType()), // If it has a type, it's not a function.
      hasParams = jsdoc.getParameterCount() > 0,
      hasReturn = jsdoc.hasReturnType(),
      hasThis = jsdoc.hasThisType(),
      hasThrownTypes = jsdoc.getThrownTypes().size() > 0,
      hasOverride = jsdoc.isOverride();

  return noType &&
         (hasParams || hasReturn || hasThis || hasThrownTypes || hasOverride);
};


var validMethodFunctions = [
  'goog.nullFunction',
  'goog.abstractMethod',
  'goog.events.Event.preventDefault',
  
  // Some deprecated methods on BaseNode are re-directed to these other methods.
  'goog.ui.tree.BaseNode.prototype.getModel',
  'goog.ui.tree.BaseNode.prototype.setModel',
  'goog.ui.tree.BaseNode.prototype.removeChild'
];
